/*
 * SPDX-License-Identifier: Apache-2.0
 * Copyright Red Hat Inc. and Hibernate Authors
 */
package org.hibernate.orm.test.pc;

import jakarta.persistence.CascadeType;
import jakarta.persistence.Entity;
import jakarta.persistence.EntityNotFoundException;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Persistence;
import jakarta.persistence.PersistenceUnitUtil;
import jakarta.persistence.PersistenceUtil;
import org.hibernate.Hibernate;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.annotations.NaturalId;
import org.hibernate.engine.spi.SessionImplementor;
import org.hibernate.metamodel.CollectionClassification;
import org.hibernate.testing.orm.junit.EntityManagerFactoryScope;
import org.hibernate.testing.orm.junit.Jpa;
import org.hibernate.testing.orm.junit.SettingProvider;
import org.junit.jupiter.api.Test;

import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

import static org.assertj.core.api.Assertions.assertThat;
import static org.hibernate.cfg.AvailableSettings.DEFAULT_LIST_SEMANTICS;

/**
 * @author Vlad Mihalcea
 */
@Jpa(
		annotatedClasses = {
				PersistenceContextTest.Person.class,
				PersistenceContextTest.Book.class,
		},
		settingProviders = @SettingProvider(
				settingName = DEFAULT_LIST_SEMANTICS,
				provider = PersistenceContextTest.CollectionClassificationProvider.class
		)
)
public class PersistenceContextTest {

	public static class CollectionClassificationProvider implements SettingProvider.Provider<CollectionClassification> {
		@Override
		public CollectionClassification getSetting() {
			return CollectionClassification.BAG;
		}
	}

	@Test
	public void test(EntityManagerFactoryScope scope) {
		scope.inTransaction( entityManager -> {
			//tag::pc-unwrap-example[]
			Session session = entityManager.unwrap( Session.class );
			SessionImplementor sessionImplementor = entityManager.unwrap( SessionImplementor.class );

			SessionFactory sessionFactory = entityManager.getEntityManagerFactory().unwrap( SessionFactory.class );
			//end::pc-unwrap-example[]
		} );
		Long _personId = scope.fromTransaction( entityManager -> {
			entityManager.createQuery( "delete from Book" ).executeUpdate();
			entityManager.createQuery( "delete from Person" ).executeUpdate();

			//tag::pc-persist-jpa-example[]
			Person person = new Person();
			person.setId( 1L );
			person.setName( "John Doe" );

			entityManager.persist( person );
			//end::pc-persist-jpa-example[]

			//tag::pc-remove-jpa-example[]
			entityManager.remove( person );
			//end::pc-remove-jpa-example[]

			entityManager.persist( person );
			Long personId = person.getId();

			//tag::pc-get-reference-jpa-example[]
			Book book = new Book();
			book.setAuthor( entityManager.getReference( Person.class, personId ) );
			//end::pc-get-reference-jpa-example[]

			return personId;
		} );
		scope.inTransaction( entityManager -> {
			Long personId = _personId;

			//tag::pc-find-jpa-example[]
			Person person = entityManager.find( Person.class, personId );
			//end::pc-find-jpa-example[]
		} );
		scope.inTransaction( entityManager -> {
			Session session = entityManager.unwrap( Session.class );
			entityManager.createQuery( "delete from Book" ).executeUpdate();
			entityManager.createQuery( "delete from Person" ).executeUpdate();

			//tag::pc-persist-native-example[]
			Person person = new Person();
			person.setId( 1L );
			person.setName( "John Doe" );

			session.persist( person );
			//end::pc-persist-native-example[]

			//tag::pc-remove-native-example[]
			session.remove( person );
			//end::pc-remove-native-example[]

			session.persist( person );
			Long personId = person.getId();

			//tag::pc-get-reference-native-example[]
			Book book = new Book();
			book.setId( 1L );
			book.setIsbn( "123-456-7890" );
			entityManager.persist( book );
			book.setAuthor( session.getReference( Person.class, personId ) );
			//end::pc-get-reference-native-example[]
		} );
		scope.inTransaction( entityManager -> {
			Session session = entityManager.unwrap( Session.class );
			Long personId = _personId;

			//tag::pc-find-native-example[]
			Person person = session.get( Person.class, personId );
			//end::pc-find-native-example[]
		} );
		scope.inTransaction( entityManager -> {
			Session session = entityManager.unwrap( Session.class );
			Long personId = _personId;

			//tag::pc-find-by-id-native-example[]
			Person person = session.byId( Person.class ).load( personId );
			//end::pc-find-by-id-native-example[]

			//tag::pc-find-optional-by-id-native-example[]
			Optional<Person> optionalPerson = session.byId( Person.class ).loadOptional( personId );
			//end::pc-find-optional-by-id-native-example[]

			String isbn = "123-456-7890";

			//tag::pc-find-by-simple-natural-id-example[]
			Book book = session.bySimpleNaturalId( Book.class ).getReference( isbn );
			//end::pc-find-by-simple-natural-id-example[]
			assertThat(book ).isNotNull();
		} );
		scope.inTransaction( entityManager -> {
			Session session = entityManager.unwrap( Session.class );
			String isbn = "123-456-7890";

			//tag::pc-find-by-natural-id-example[]
			Book book = session
					.byNaturalId( Book.class )
					.using( "isbn", isbn )
					.load();
			//end::pc-find-by-natural-id-example[]
			assertThat(book ).isNotNull();

			//tag::pc-find-optional-by-simple-natural-id-example[]
			Optional<Book> optionalBook = session
					.byNaturalId( Book.class )
					.using( "isbn", isbn )
					.loadOptional();
			//end::pc-find-optional-by-simple-natural-id-example[]
		} );

		scope.inTransaction( entityManager -> {
			Long personId = _personId;

			//tag::pc-managed-state-jpa-example[]
			Person person = entityManager.find( Person.class, personId );
			person.setName( "John Doe" );
			entityManager.flush();
			//end::pc-managed-state-jpa-example[]
		} );

		scope.inTransaction( entityManager -> {
			Long personId = _personId;

			//tag::pc-refresh-jpa-example[]
			Person person = entityManager.find( Person.class, personId );

			entityManager.createQuery( "update Person set name = UPPER(name)" ).executeUpdate();

			entityManager.refresh( person );
			assertThat( person.getName()).isEqualTo("JOHN DOE");
			//end::pc-refresh-jpa-example[]
		} );

		try {
			scope.inTransaction( entityManager -> {
				Long personId = _personId;

				//tag::pc-refresh-child-entity-jpa-example[]
				try {
					Person person = entityManager.find( Person.class, personId );

					Book book = new Book();
					book.setId( 100L );
					book.setTitle( "Hibernate User Guide" );
					book.setAuthor( person );
					person.getBooks().add( book );

					entityManager.refresh( person );
				}
				catch (EntityNotFoundException expected) {
					// "Beware when cascading the refresh associations to transient entities!"
				}
				//end::pc-refresh-child-entity-jpa-example[]
			} );
		}
		catch (Exception expected) {
		}

		scope.inTransaction( entityManager -> {
			Session session = entityManager.unwrap( Session.class );
			Long personId = _personId;

			//tag::pc-managed-state-native-example[]
			Person person = session.byId( Person.class ).load( personId );
			person.setName( "John Doe" );
			session.flush();
			//end::pc-managed-state-native-example[]
		} );

		scope.inTransaction( entityManager -> {
			Session session = entityManager.unwrap( Session.class );
			Long personId = _personId;

			//tag::pc-refresh-native-example[]
			Person person = session.byId( Person.class ).load( personId );

			session.doWork( connection -> {
				try (Statement statement = connection.createStatement()) {
					statement.executeUpdate( "UPDATE Person SET name = UPPER(name)" );
				}
			} );

			session.refresh( person );
			assertThat( person.getName()).isEqualTo("JOHN DOE");
			//end::pc-refresh-native-example[]
		} );

		scope.inTransaction( entityManager -> {
			Session session = entityManager.unwrap( Session.class );
			Long personId = _personId;

			Person personDetachedReference = session.byId( Person.class ).load( personId );
			//Clear the Session so the person entity becomes detached
			session.clear();
			new MergeVisualizer( session ).merge( personDetachedReference );
		} );

		scope.inTransaction( entityManager -> {
			Long personId = _personId;

			//tag::pc-merge-jpa-example[]
			Person person = entityManager.find( Person.class, personId );
			//Clear the EntityManager so the person entity becomes detached
			entityManager.clear();
			person.setName( "Mr. John Doe" );

			person = entityManager.merge( person );
			//end::pc-merge-jpa-example[]

			//tag::pc-contains-jpa-example[]
			boolean contained = entityManager.contains( person );
			//end::pc-contains-jpa-example[]
			assertThat( contained ).isTrue();

			//tag::pc-verify-lazy-jpa-example[]
			PersistenceUnitUtil persistenceUnitUtil = entityManager.getEntityManagerFactory().getPersistenceUnitUtil();

			boolean personInitialized = persistenceUnitUtil.isLoaded( person );

			boolean personBooksInitialized = persistenceUnitUtil.isLoaded( person.getBooks() );

			boolean personNameInitialized = persistenceUnitUtil.isLoaded( person, "name" );
			//end::pc-verify-lazy-jpa-example[]
		} );

		scope.inTransaction( entityManager -> {
			Long personId = _personId;

			Person person = entityManager.find( Person.class, personId );

			//tag::pc-verify-lazy-jpa-alternative-example[]
			PersistenceUtil persistenceUnitUtil = Persistence.getPersistenceUtil();

			boolean personInitialized = persistenceUnitUtil.isLoaded( person );

			boolean personBooksInitialized = persistenceUnitUtil.isLoaded( person.getBooks() );

			boolean personNameInitialized = persistenceUnitUtil.isLoaded( person, "name" );
			//end::pc-verify-lazy-jpa-alternative-example[]
		} );

		scope.inTransaction( entityManager -> {
			Session session = entityManager.unwrap( Session.class );
			Long personId = _personId;

			//tag::pc-merge-native-example[]
			Person person = session.byId( Person.class ).load( personId );
			//Clear the Session so the person entity becomes detached
			session.clear();
			person.setName( "Mr. John Doe" );

			person = (Person) session.merge( person );
			//end::pc-merge-native-example[]

			//tag::pc-contains-native-example[]
			boolean contained = session.contains( person );
			//end::pc-contains-native-example[]
			assertThat( contained ).isTrue();

			//tag::pc-verify-lazy-native-example[]
			boolean personInitialized = Hibernate.isInitialized( person );

			boolean personBooksInitialized = Hibernate.isInitialized( person.getBooks() );

			boolean personNameInitialized = Hibernate.isPropertyInitialized( person, "name" );
			//end::pc-verify-lazy-native-example[]
		} );
	}

	public static class MergeVisualizer {
		private final Session session;

		public MergeVisualizer(Session session) {
			this.session = session;
		}

		//tag::pc-merge-visualize-example[]
		public Person merge(Person detached) {
			Person newReference = session.byId( Person.class ).load( detached.getId() );
			newReference.setName( detached.getName() );
			return newReference;
		}
		//end::pc-merge-visualize-example[]
	}

	@Entity(name = "Person")
	public static class Person {

		@Id
		private Long id;

		private String name;

		@OneToMany(mappedBy = "author", cascade = CascadeType.ALL)
		private List<Book> books = new ArrayList<>();

		public Long getId() {
			return id;
		}

		public void setId(Long id) {
			this.id = id;
		}

		public String getName() {
			return name;
		}

		public void setName(String name) {
			this.name = name;
		}

		public List<Book> getBooks() {
			return books;
		}
	}

	//tag::pc-find-by-natural-id-entity-example[]
	@Entity(name = "Book")
	public static class Book {

		@Id
		private Long id;

		private String title;

		@NaturalId
		private String isbn;

		@ManyToOne
		private Person author;

		//Getters and setters are omitted for brevity

		//end::pc-find-by-natural-id-entity-example[]

		public Long getId() {
			return id;
		}

		public void setId(Long id) {
			this.id = id;
		}

		public String getTitle() {
			return title;
		}

		public void setTitle(String title) {
			this.title = title;
		}

		public Person getAuthor() {
			return author;
		}

		public void setAuthor(Person author) {
			this.author = author;
		}

		public String getIsbn() {
			return isbn;
		}

		public void setIsbn(String isbn) {
			this.isbn = isbn;
		}
		//tag::pc-find-by-natural-id-entity-example[]
	}
	//end::pc-find-by-natural-id-entity-example[]
}
